package com.my.study.lock;

import com.my.study.lock.aop.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 切面
 *
 * @author: yidujun
 * @create: 2021/03/15 15:30
 */
@Aspect
@Component
@Slf4j
public class RedisLockAspect {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 存储目前有效的key定义
     */
    private static ConcurrentLinkedQueue<RedisLockDefinitionHolder> holderList = new ConcurrentLinkedQueue<>();

    /**
     * 线程池，维护keyAliveTime
     */
    private static final ScheduledExecutorService SCHEDULER = new ScheduledThreadPoolExecutor(1,
            new BasicThreadFactory.Builder().namingPattern("redisLock-schedule-pool").daemon(true).build());

    // 每个类创建对象的时候调用一次
    {
        SCHEDULER.scheduleAtFixedRate(() -> {
            try {
                Iterator<RedisLockDefinitionHolder> iterator = holderList.iterator();
                while (iterator.hasNext()) {
                    RedisLockDefinitionHolder holder = iterator.next();
                    // 判空 || 判断 key 是否还有效，无效的话进行移除
                    if (Objects.isNull(holder) || (redisTemplate.opsForValue().get(holder.getBusinessKey()) == null)) {
                        holderList.remove();
                        continue;
                    }

                    // 超时重试次数，超过时给线程设定中断
                    if (holder.getCurrentCount() > holder.getTryCount()) {
                        holder.getCurrentThread().interrupt();
                        holderList.remove();
                        continue;
                    }

                    long curTime = System.currentTimeMillis();
                    boolean shouldExtend = holder.getLastModifyTime() + holder.getModifyPeriod() <= curTime;
                    if (shouldExtend) {
                        holder.setLastModifyTime(curTime);
                        redisTemplate.expire(holder.getBusinessKey(), holder.getLockTime(), TimeUnit.SECONDS);
                        log.info("businessKey : [" + holder.getBusinessKey() + "], try count : " + holder.getCurrentCount());
                        holder.setCurrentCount(holder.getCurrentCount() + 1);
                    }

                }
            } catch (Exception e) {
                log.error("定时任务异常,原因：", e);
            }

        }, 0, 2, TimeUnit.SECONDS);
    }

    @Pointcut("@annotation(com.my.study.lock.aop.RedisLock)")
    public void redisLockPC() {
    }

    @Around(value = "redisLockPC()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Method method = resolveMethod(point);
        RedisLock annotation = method.getAnnotation(RedisLock.class);
        RedisLockTypeEnum typeEnum = annotation.typeEnum();
        Object[] params = point.getArgs();
        String ukString = params[annotation.lockFiled()].toString();
        String businessKey = typeEnum.getUniqueKey(ukString);
        String uniqueValue = UUID.randomUUID().toString();
        Object result = null;
        try {
            boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(businessKey, uniqueValue);
            if (!isSuccess) {
                throw new Exception("You can't do it，because another has get the lock =-=");
            }
            redisTemplate.expire(businessKey, annotation.lockTime(), TimeUnit.SECONDS);
            Thread currentThread = Thread.currentThread();
            holderList.add(new RedisLockDefinitionHolder(businessKey, annotation.lockTime(), System.currentTimeMillis(),
                    currentThread, annotation.tryCount()));
            result = point.proceed();
            // 线程被中断，抛出异常，中断此次请求
            if (currentThread.isInterrupted()) {
                throw new InterruptedException("You had been interrupted =-=");
            }
        } catch (InterruptedException e) {
            log.error("Interrupt exception, rollback transaction", e);
            throw new Exception("Interrupt exception, please send request again");
        } catch (Exception e) {
            log.error("has some error, please check again", e);
        } finally {
            // 请求结束后，强制删掉 key，释放锁
            redisTemplate.delete(businessKey);
            log.info("release the lock, businessKey is [" + businessKey + "]");
        }

        return result;
    }

    /**
     * 获取目标函数Method
     *
     * @param point
     * @return
     */
    private Method resolveMethod(final ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<?> targetClass = point.getTarget().getClass();
        Method method = getDeclaredMethodFor(
                targetClass,
                signature.getName(),
                signature.getMethod().getParameterTypes());
        if (null == method) {
            throw new IllegalStateException("Cannot resolve target method: " + signature.getMethod().getName());
        }
        return method;
    }

    /**
     * 获取指定类上的指定方法
     *
     * @param clazz 指定类
     * @param name 指定方法名
     * @param parameterTypes 参数类型列表
     * @return 找到返回对应Method,否则返回null
     * @author Carlos
     * @date 2020/12/31 15:53
     */
    public static Method getDeclaredMethodFor(Class<?> clazz, String name, Class<?>... parameterTypes) {
        try {
            return clazz.getDeclaredMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            // 获取父类
            Class<?> superclass = clazz.getSuperclass();
            if (null != superclass) {
                return getDeclaredMethodFor(superclass, name, parameterTypes);
            }
        }
        return null;
    }
}
